﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    /// <summary>
    /// Standard analyzer for CLR projects. Uses a combination of reflection to load types and IL parsing to analyze method bodies.
    /// Wenn loading a type through reflection the types initializer is called. This might have implications, when initializers
    /// actually perform tasks.
    /// </summary>
    public class Analyzer : IAnalyzer
    {
        private readonly IDependencyModel _dependencyModel;

        private readonly HashSet<string> _loadedAssemblies = new HashSet<string>();

        private readonly object _lockAddNamespace = new object();
        private readonly object _lockAddType = new object();

        static Analyzer()
        {
            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;
        }

        public Analyzer(IDependencyModel model)
        {
            _dependencyModel = model;

        }

        private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
        {
            var domain = (AppDomain)sender;
            foreach (var assembly in domain.GetAssemblies())
            {
                if (assembly.FullName == args.Name)
                    return assembly;
            }
            return null;
        }

        public void BuildDependencies(IEnumerable<Assembly> assemblies)
        {
            DescriptorBuilder.FlushCache();
            var xamlAnalyzer = new XamlAnalyzer { Model = _dependencyModel };
            var modules = assemblies as Assembly[] ?? assemblies.ToArray();
            foreach (var module in modules)
            {
                if (module != null)
                {
                    AnalyzeModule(module);
                }
            }
            xamlAnalyzer.Analyze(modules);
            AnalyzeInheritance();
        }

        private void AnalyzeInheritance()
        {
            foreach (TypeDescriptor typeDescriptor in _dependencyModel.DependenciesPerType.Keys)
            {
                var type = typeDescriptor.Type;
                if (type == null) continue;
                var baseType = type.BaseType;
                AddVariants(baseType, typeDescriptor);
                foreach (var interfaceType in type.GetInterfaces())
                {
                    AddVariants(interfaceType, typeDescriptor);
                }
            }
        }

        private void AddVariants(Type baseType, TypeDescriptor typeDescriptor)
        {
            if (baseType == null) return;
            var baseDescriptor = DescriptorBuilder.BuildTypeDescriptor(baseType);
            baseDescriptor = _dependencyModel.DependenciesPerType.Keys.FirstOrDefault(x => x.Equals(baseDescriptor)) as TypeDescriptor;
            if (baseDescriptor == null) return;
            if (baseDescriptor.ImplementingTypes == null) baseDescriptor.ImplementingTypes = new List<TypeDescriptor>();
            if (!Equals(baseDescriptor, typeDescriptor)) baseDescriptor.ImplementingTypes.Add(typeDescriptor);
            if (typeDescriptor.InheritedTypes == null) typeDescriptor.InheritedTypes = new List<TypeDescriptor>();
            if (!Equals(baseDescriptor, typeDescriptor)) typeDescriptor.InheritedTypes.Add(baseDescriptor);
        }

        public void BuildDependencies(IEnumerable<string> assemblyFiles)
        {
            var assemblies = assemblyFiles.Select(x =>
            {
                try
                {
                    return Assembly.LoadFile(x);
                }
                catch (NotSupportedException e)
                {
                    try
                    {
                        return Assembly.ReflectionOnlyLoadFrom(x);

                    }
                    catch (Exception e1)
                    {
                        
                        Console.WriteLine(e1);
                    }
                }
                return null;
            }).Where(module => module != null).ToArray();
            BuildDependencies(assemblies);
        }

        private void AnalyzeModule(_Assembly module)
        {
            var referencedAssemblies = module.GetReferencedAssemblies();
            foreach (var reference in referencedAssemblies)
            {
                if (_loadedAssemblies.Contains(reference.FullName)) continue;
                var fileName = Path.GetDirectoryName(module.Location) + "\\" + reference.Name;
                try
                {
                    if (File.Exists(fileName + ".dll"))
                    {
                        Assembly.LoadFrom(fileName + ".dll");
                    }
                    else if (File.Exists(fileName + ".exe"))
                    {
                        Assembly.LoadFrom(fileName + ".exe");
                    }
                    _loadedAssemblies.Add(reference.FullName);
                }
                catch (Exception)
                {
                }
            }

            Type[] types;
            try
            {
                types = module.GetTypes();
            }
            catch (ReflectionTypeLoadException e)
            {
                types = e.Types;
            }
            catch
            {
                return;
            }
            var assemblyDescriptor = DescriptorBuilder.BuildModuleDescriptor(module);
            assemblyDescriptor.IsPartOfProject = true;
            _dependencyModel.Add(assemblyDescriptor);
            Parallel.ForEach(types, type =>
            {
                if (type != null)
                {
                    AnalyzeType(type, assemblyDescriptor);
                }
            });
        }

        private void AnalyzeType(Type type, ModuleDescriptor moduleDescriptor)
        {
            MemberInfo[] members;
            try
            {
                members = type.GetMembers(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Static);
            }
            catch (Exception)
            {
                return;
            }
            var typeNamespace = DescriptorBuilder.ExtractNamespaceFromType(type);
            var namespaceDescriptor = DescriptorBuilder.BuildSubmoduleDescriptor(moduleDescriptor, typeNamespace);
            if (namespaceDescriptor.FullName == "XamlGeneratedNamespace") return;
            lock (_lockAddNamespace)
            {
                if (!moduleDescriptor.Submodules.Contains(namespaceDescriptor))
                {
                    moduleDescriptor.Submodules.Add(namespaceDescriptor);
                }
            }
            _dependencyModel.Add(namespaceDescriptor);
            var typeDescriptor = DescriptorBuilder.BuildTypeDescriptor(type, namespaceDescriptor);
            _dependencyModel.Add(typeDescriptor);
            lock (_lockAddType)
            {
                if (!namespaceDescriptor.Types.Contains(typeDescriptor))
                    namespaceDescriptor.Types.Add(typeDescriptor);
            }
            AnalyzeAttributes(type, typeDescriptor);

            var typeMember = DescriptorBuilder.BuildMemberDescriptor(type, typeDescriptor, true);
            AnalyzeBaseTypeReferences(type, typeMember);
            if (typeDescriptor.FullName != null)
            {
                int splitIndex = typeDescriptor.FullName.IndexOf("+<");
                if (splitIndex>0)
                {
                    var originatingTypeName = typeDescriptor.FullName.Substring(0, splitIndex);
                    typeDescriptor.OriginatingType = typeDescriptor.Submodule.Types.FirstOrDefault(x => Equals(x.FullName, originatingTypeName));
                }
            }

            int methodCount = 0;
            int fieldCount = 0;
            foreach (MemberInfo member in members)
            {
                var field = member as FieldInfo;
                if (field != null)
                {
                    AnalyzeField(field, typeDescriptor);
                    fieldCount++;
                    continue;
                }

                var method = member as MethodBase;
                if (method != null)
                {
                    AnalyzeMethod(method, typeDescriptor);
                    if (!method.Name.StartsWith("get_") && !method.Name.StartsWith("set_") && !Equals(method.Name, ".ctor") && !Equals(method.Name, ".cctor"))
                    {
                        methodCount++;
                    }
                }
            }
            typeDescriptor.IsDataObject = methodCount < 5 && methodCount < fieldCount;
        }

        private void AnalyzeBaseTypeReferences(Type type, MemberDescriptor typeMember)
        {
            if (type.BaseType != null)
            {
                var dependencyDescriptor = DescriptorBuilder.
                    BuildDependencyDescriptorForTypeReference(type.BaseType, typeMember, ReferenceType.AccessType);
                _dependencyModel.Add(dependencyDescriptor);
            }
            foreach (var interfaceType in type.GetInterfaces())
            {
                var dependencyDescriptor = DescriptorBuilder.
                    BuildDependencyDescriptorForTypeReference(interfaceType, typeMember, ReferenceType.AccessType);
                _dependencyModel.Add(dependencyDescriptor);
            }
        }

        private void AnalyzeMethod(MethodBase method, TypeDescriptor typeDescriptor)
        {
            var memberDescriptor = DescriptorBuilder.BuildMemberDescriptor(method, typeDescriptor, false);
            memberDescriptor.IsDefinedByInterface = ReflectionUtilities.IsMemberDefinedInInterface(method);
            memberDescriptor.IsGenerated = ReflectionUtilities.IsMemberGenerated(method);
            memberDescriptor.IsOverride = ReflectionUtilities.IsMemberOverride(method);
            memberDescriptor.IsStatic = method.IsStatic;
            IList<Instruction> instList;
            try
            {
                AnalyzeAttributes(method, memberDescriptor);
                AnalyzeParameters(method, memberDescriptor);
                var methodInfo = method as MethodInfo;
                if (methodInfo != null)
                {
                    var targetType = methodInfo.ReturnType;
                    if (targetType != typeof(void) && !targetType.IsGenericParameter)
                    {
                        var dependencyDescriptor =
                            DescriptorBuilder.BuildDependencyDescriptorForTypeReference(targetType, memberDescriptor,
                                                                                        ReferenceType.Attribute);
                        _dependencyModel.Add(dependencyDescriptor);
                    }
                }
                int variableCount;
                instList = LoadInstructions(memberDescriptor, method, out variableCount);
                _dependencyModel.InstructionCache.SetInstructions(memberDescriptor, instList);
                memberDescriptor.VariableCount = variableCount;
            }
            catch (Exception)
            {
                return;
            }
            if (instList != null)
            {
                foreach (Instruction instruction in instList)
                {
                    AnalyzeInstruction(instruction, memberDescriptor);
                }
                AnalyzeExceptions(method, memberDescriptor);
            }
            typeDescriptor.Members.Add(memberDescriptor);
        }

        private void AnalyzeExceptions(MethodBase method, MemberDescriptor memberDescriptor)
        {
            MethodBody body;
            try
            {
                body = method.GetMethodBody();
            }
            catch (TypeLoadException)
            {
                return;
            }
            if (body == null)
            {
                return;
            }
            foreach (var clause in body.ExceptionHandlingClauses)
            {
                Type catchType;
                try
                {
                    catchType = clause.CatchType;
                }
                catch (InvalidOperationException)
                {
                    // TODO How come? is a "non-clause" clause used for special cases?
                    continue;
                }
                var dependencyDescriptor =
                    DescriptorBuilder.BuildDependencyDescriptorForTypeReference(catchType, memberDescriptor,
                                                                                ReferenceType.Attribute);
                _dependencyModel.Add(dependencyDescriptor);

            }
        }

        private static readonly IDictionary<OperationCode, Func<Instruction, int>> ArgumentOpcodeToIndex =
            new Dictionary<OperationCode, Func<Instruction, int>>
        {
            { OperationCode.Ldarg_0, x => 0 },
            { OperationCode.Ldarg_1, x => 1 },
            { OperationCode.Ldarg_2, x => 2 },
            { OperationCode.Ldarg_3, x => 3 },
            { OperationCode.Ldarg, x => (int)(ulong)x.OperandNonSerialized },
            { OperationCode.Ldarga, x => (int)(ulong)x.OperandNonSerialized },
        };

        public IList<Instruction> LoadInstructions(MemberDescriptor target, MemberInfo memberInfo, out int variableCount)
        {
            variableCount = 0;
            if (memberInfo as MethodBase == null)
            {
                return new List<Instruction>();
            }

            var cilParser = new CilParser { IgnoreNop = true };
            try
            {
                var readInstructions = cilParser.ReadInstructions(memberInfo as MethodBase);
                variableCount = cilParser.CurrentVariableCount;
                var parameters = cilParser.Parameters;
                foreach (var instruction in readInstructions)
                {
                    if (InstructionCache.ArgumentOpCodes.Contains(instruction.OpCode))
                    {
                        if (instruction.OpCode == OperationCode.Ldarg_S)
                        {
                            var type = instruction.OperandNonSerialized as Type;
                            if (type != null)
                            {
                                instruction.Operand = DescriptorBuilder.BuildTypeDescriptor(type);
                            }
                        }
                        else if (!target.IsStatic && instruction.OpCode == OperationCode.Ldarg_0)
                        {
                            instruction.Operand = target.ImplementingType;
                        }
                        else
                        {
                            int index = ArgumentOpcodeToIndex[instruction.OpCode](instruction) - (target.IsStatic ? 0 : 1);
                            instruction.Operand = DescriptorBuilder.BuildTypeDescriptor(parameters[index].ParameterType);
                        }
                    }
                }
                return readInstructions;
            }
            catch (Exception)
            {
                return new List<Instruction>();
            }
        }

        private void AnalyzeInstruction(Instruction inst, MemberDescriptor memberDescriptor)
        {
            if (inst.OpCode == OperationCode._Locals)
            {
                var locals = inst.OperandNonSerialized as IList<LocalVariableInfo>;
                if (locals != null)
                {
                    foreach (var l in locals)
                    {
                        var targetType = l.LocalType;
                        if (targetType.IsGenericParameter) return;
                        var dependencyDescriptor = DescriptorBuilder.BuildDependencyDescriptorForTypeReference(targetType, memberDescriptor, ReferenceType.Variable);
                        _dependencyModel.Add(dependencyDescriptor);
                    }
                    return;
                }
            }
            if (InstructionCache.TypeOpCodes.Contains(inst.OpCode))
            {
                var targetType = inst.OperandNonSerialized as Type;
                if (targetType != null)
                {
                    if (targetType.IsGenericParameter) return;
                    var dependencyDescriptor = DescriptorBuilder.BuildDependencyDescriptorForTypeReference(targetType, memberDescriptor, ReferenceType.AccessType);
                    _dependencyModel.Add(dependencyDescriptor);
                    inst.Operand = dependencyDescriptor.Target.ImplementingType;
                    return;
                }
            }

            if (InstructionCache.FieldOpCodes.Contains(inst.OpCode) ||
                InstructionCache.MethodOpCodes.Contains(inst.OpCode))
            {
                var member = inst.OperandNonSerialized as MemberInfo;
                if (member != null)
                {
                    var dependencyDescriptor = DescriptorBuilder.BuildDependencyDescriptorForMember(member, memberDescriptor, ReferenceType.AccessMember);
                    _dependencyModel.Add(dependencyDescriptor);
                    inst.Operand = dependencyDescriptor.Target;
                }
            }
        }

        private static readonly ISet<Type> GeneratedAttributes = new HashSet<Type>
        {
            typeof(CompilerGeneratedAttribute),
            typeof(GeneratedCodeAttribute)
        };

        private void AnalyzeAttributes(MemberInfo member, MemberDescriptor memberDescriptor)
        {
            var attributes = member.GetCustomAttributes(true);

            foreach (Attribute attribute in attributes)
            {
                var targetType = attribute.GetType();
                if (GeneratedAttributes.Contains(targetType) && !((member.Name.StartsWith("get_") || member.Name.StartsWith("set_")) && targetType == typeof(CompilerGeneratedAttribute)))
                {
                    memberDescriptor.IsGenerated = true;
                }
                var dependencyDescriptor = DescriptorBuilder.BuildDependencyDescriptorForTypeReference(targetType, memberDescriptor, ReferenceType.Attribute);
                _dependencyModel.Add(dependencyDescriptor);
            }
        }

        private void AnalyzeAttributes(Type type, TypeDescriptor typeDescriptor)
        {
            bool isLinq = !string.IsNullOrEmpty(type.FullName) && type.FullName.Contains(DescriptorBuilder.LinqIdentifier);
            if (isLinq)
            {
                return;
            }
            try
            {
                var attributes = type.GetCustomAttributes(true);

                foreach (Attribute attribute in attributes)
                {
                    var targetType = attribute.GetType();
                    if (GeneratedAttributes.Contains(targetType))
                    {
                        typeDescriptor.IsGenerated = true;
                    }
                }
            }
            catch (InvalidOperationException)
            {
            }
        }

        private void AnalyzeParameters(MethodBase member, MemberDescriptor memberDescriptor)
        {
            var parameters = member.GetParameters();
            memberDescriptor.ParameterCount = parameters.Count();
            foreach (var parameter in parameters)
            {
                var targetType = parameter.ParameterType;
                if (targetType.IsGenericParameter) continue;
                var dependencyDescriptor = DescriptorBuilder.BuildDependencyDescriptorForTypeReference(targetType, memberDescriptor, ReferenceType.Parameter);
                _dependencyModel.Add(dependencyDescriptor);
            }
        }

        private void AnalyzeField(FieldInfo field, TypeDescriptor typeDescriptor)
        {
            if (field.Name == "value__" || field.IsLiteral) return;
            var memberDescriptor = DescriptorBuilder.BuildMemberDescriptor(field, typeDescriptor, false);
            memberDescriptor.IsStatic = field.IsStatic;
            Type targetType = null;
            try
            {
                targetType = field.FieldType;
                if (targetType.IsGenericParameter) return;
            }
            catch (TypeLoadException)
            {
            }
            var dependencyDescriptor = DescriptorBuilder.BuildDependencyDescriptorForTypeReference(targetType, memberDescriptor, ReferenceType.Field);
            _dependencyModel.Add(dependencyDescriptor);
            typeDescriptor.Members.Add(memberDescriptor);
        }
    }
}
